Skip to content

feat(sql): add x402 and MPP payment lanes - #60

Draft
johnpmitsch wants to merge 2 commits into
mainfrom
dx-6602-add-x402mpp-support-for-sql-explorer
Draft

feat(sql): add x402 and MPP payment lanes#60
johnpmitsch wants to merge 2 commits into
mainfrom
dx-6602-add-x402mpp-support-for-sql-explorer

Conversation

@johnpmitsch

@johnpmitsch johnpmitsch commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Adds SQL Explorer without a Quicknode account: wallet, fund, query. The account-key path stays.

qn sql clusters and qn sql schema always hit the public catalog. Only qn sql query chooses who pays: an API key, --x402-drawdown, or --mpp-session. Config never turns a payment path on.

qn micropayments (alias pay) is the shared funding noun. qn rpc x402 and qn rpc mpp stay first-class and call the same runners.

# Keyless: generate a wallet, fund it, query.
qn wallet generate --chain eip155:84532
qn micropayments x402 buy-credits \
  --payment-key-file ./key.txt \
  --payment-network eip155:84532 \
  --payment-asset 0x036CbD53842c5426634e7929541eC2318f3dCF7e \
  --max-amount 10000000 --network base-sepolia --yes
qn sql query "SELECT 1" \
  --cluster-id hyperliquid-core-mainnet \
  --x402-drawdown \
  --payment-key-file ./key.txt \
  --payment-network base-sepolia

Channel accounting on the MPP lane

The paid query saves the channel on both the success and error paths, before mapping the error:

// Persist before inspecting: a failed query can still have spent the voucher.
let result = ctx
    .sdk
    .sql
    .query_with_mpp_session(&params, &payment, &mut channel)
    .await;
if let Some(path) = &channels_path {
    let _ = config::save_channel(path, &scope, &channel);
}
let result = result.map_err(map_sql_mpp_error)?;

The SDK advances channel whenever the voucher reached the gateway. Previously the CLI returned on error before saving, so a query that failed after the gateway banked the voucher left the cached cumulative behind the gateway's accepted value; the next query re-signed that stale value and was refused, stranding the channel.

Error matching

Both payment matchers now gate on the status. The body markers were ||-alternatives to the 402 check, so a 500 whose message happened to contain "insufficient" reported as an empty balance:

// Status only: body text on a 500 saying "insufficient" is not an empty balance.
fn is_sql_requires_payment(e: &SdkError) -> bool {
    matches!(e, SdkError::Api { status, .. } if status.as_u16() == 402)
}

Docs

  • sql query has no per-request --x402/--mpp flag; context.md no longer says the flag is "rejected" (clap emits a generic unknown-argument error).
  • The no-payer sql query is documented as exit 1. A key is one of three ways to pay, so a missing payer is a usage error, not no API key found (exit 4, which stays the default for key-required commands).

Dependency

Pinned to quicknode/sdk@0bd4f3c (quicknode/sdk#60), git-pinned until that crate is published. A release cannot ship from a git rev.

Closes DX-6602

Test plan

  • cargo test --test sql --test micropayments --test rpc_payment --test cli_smoke — 86 passed
  • cargo test — 462 passed
  • cargo clippy --all-targets -- -D warnings
  • cargo fmt --check
  • CI

New coverage: a failed MPP query persists the receipt cumulative; sql clusters maps a catalog error to exit 2; buy-credits clears the confirmation gate with --yes; insta snapshot for the sql clusters table.

SQL Explorer can run without an account: clusters and schema stay
public, and query pays with x402 drawdown or an MPP session.
qn micropayments (alias pay) shares the existing x402 and mpp runners.
The MPP query path returned on error before saving the channel, so a
query that failed after the gateway banked the voucher left the cached
cumulative behind the gateway's accepted value. The next query re-signed
that stale cumulative and was refused, stranding the channel. The SDK
now advances the channel whenever the voucher reached the gateway, and
this saves it on both paths before mapping the error.

Also gates the credit and channel error matchers on a 402: the body
markers were alternatives to the status check, so a 500 whose message
happened to contain "insufficient" reported as an empty balance.

Docs: SQL has no per-request --x402 flag, so context.md no longer claims
it is rejected; the no-payer query is documented as exit 1, since a key
is one of three ways to pay; drops an absence-of-feature line from the
README MPP intro.

Tests: a failed query persists the receipt cumulative, sql clusters maps
a catalog error to exit 2, buy-credits clears the gate with --yes, and a
clusters table snapshot.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant